<template>
  <div class="graph-chart-view"></div>
</template>

<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator'
import G6, { GraphOptions, IEdge } from '@antv/g6'
import Chart from '@antv/chart-node-g6'
import { isEqual } from 'lodash'
import { scaleLinear } from 'd3-scale'

G6.registerNode(
  'node-with-line',
  {
    draw(cfg, group) {
      let subChartWidth = 240
      let subChartHeight = 120
      if (cfg?.subChartOptions) {
        const { subWidthSize }: any = cfg.subChartOptions
        subChartWidth = subWidthSize
        subChartHeight = Math.ceil(subChartWidth / 2)
      }

      const keyShape = group!.addShape('rect', {
        attrs: {
          x: -40,
          y: -30,
          width: subChartWidth,
          height: subChartHeight,
          stroke: '#CCCCCC',
          fill: 'white',
          radius: 5,
        },
      })

      group!.addShape('text', {
        attrs: {
          textBaseline: 'top',
          y: -20,
          x: -30,
          text: cfg?.label,
          fill: '#000',
          fontSize: 20,
        },
        name: 'title',
      })

      if (group) {
        const view = new Chart({
          group,
          width: subChartWidth - 120,
          height: subChartHeight - 75,
          x: 30,
          y: 10,
        })
        view.axis(true)
        keyShape.set('lineView', view)
        if (cfg?.subChartOptions) {
          const { subChartOptions }: any = cfg
          const data = subChartOptions.value || []
          const { labelKey, valueKey, lineSmooth, colors } = subChartOptions

          if (data && labelKey && valueKey) {
            view
              .axis(labelKey, {
                label: {
                  style: {
                    fontSize: 20,
                  },
                },
              })
              .axis(valueKey, {
                label: {
                  style: {
                    fontSize: 20,
                  },
                },
              })
              .line()
              .position(`${labelKey}*${valueKey}`)
              .shape(lineSmooth ? 'smooth' : 'line')
              .color(colors[0])
            view.data(data)
            view.render()
          }
        }
      }

      return keyShape
    },
    update: undefined,
  },
  'single-node'
)

interface IKeyValue {
  [key: string]: any
}

@Component
export default class Graph extends Vue {
  @Prop({ default: () => ({}) }) config!: IKeyValue
  @Prop({ default: '' }) chartType!: string

  private graphInstance: any = null // graph 实例
  private titleHeight: number = 38

  mounted() {
    if (!this.graphInstance) {
      this.initGraph()
    }
  }

  @Watch('config', { deep: true, immediate: true })
  getConfig(newValue: IKeyValue, oldValue: IKeyValue) {
    if (this.graphInstance) {
      this.updateGraph(newValue, oldValue)
    } else {
      this.initGraph()
    }
  }

  /**
   * 初始化graph 图表
   */
  private initGraph() {
    if (this.chartType === 'dagre') {
      this.initDagre()
    } else if (this.chartType === 'graphLineChart') {
      this.renderLineGraph()
    }
  }

  private updateGraph(newConfig: IKeyValue, oldConfig: IKeyValue) {
    if (this.chartType === 'dagre') {
      this.updateDagre(newConfig, oldConfig)
    } else if (this.chartType === 'graphLineChart') {
      this.updateLineGraph()
    }
  }

  private get graphPadding() {
    // const { title } = this.config
    let paddingH = 0 // 横向 padding
    let paddingV = 0 // 纵向padding
    if (this.config.padding && this.config.padding.length > 0) {
      paddingH = (this.config.padding[1] || 0) + (this.config.padding[3] || 0)
      paddingV = (this.config.padding[0] || 0) + (this.config.padding[2] || 0)
    }
    // if (title.trim()) {
    //   paddingV += this.titleHeight // title 高度
    // }

    return [paddingH, paddingV]
  }

  /**
   * 转换数据到G6 dagre接受的格式
   */
  private parseDagreData({
    nodes = [],
  }: // edges = [],
  {
    nodes: IKeyValue[]
    edges: IKeyValue[]
  }) {
    const { categoryId } = nodes[0]
    const { nodeType, edgeType } = this.config
    let newNodes = categoryId
      ? nodes.filter((n) => n.categoryId === categoryId)
      : nodes
    // const nodeIdSet = new Set(newNodes.map((n) => n.id))
    const combosSet = new Set(
      newNodes.map(
        (node) => node.attrs.filter((a: any) => a.key === 'level')[0]?.value
      )
    )
    const comboMapping = {
      up: '上 游',
      middle: '中 游',
      down: '下 游',
    }
    newNodes = newNodes.map((node) => {
      return {
        id: node.id,
        label: node.label,
        attrs: node.attrs,
        categoryId: node.categoryId,
        type: nodeType,
        comboId: node.attrs.filter((a: any) => a.key === 'level')[0]?.value, // 上中下游combo
      }
    })
    const firstUpNode = newNodes.filter((node) => node.comboId === 'up')[0]
    const firstMiddleNode = newNodes.filter(
      (node) => node.comboId === 'middle'
    )[0]
    const firstDownNode = newNodes.filter((node) => node.comboId === 'down')[0]
    // const newEdges = edges
    //   .filter((edge) => {
    //     return nodeIdSet.has(edge.source) && nodeIdSet.has(edge.target)
    //   })
    //   .map((edge) => ({
    //     id: edge.id,
    //     type: edgeType,
    //     source: edge.source,
    //     target: edge.target,
    //   }))
    return {
      nodes: newNodes,
      edges: [
        {
          source: firstUpNode.id,
          target: firstMiddleNode.id,
          type: edgeType,
        },
        {
          source: firstMiddleNode.id,
          target: firstDownNode.id,
          type: edgeType,
        },
      ],
      // edges: newEdges,
      combos: [...combosSet].map((combo: 'up' | 'middle' | 'down') => ({
        id: combo,
        label: comboMapping[combo],
      })),
    }
  }

  /**
   * 初始化graph 流程图
   */
  private initDagre() {
    if (
      !this.$el ||
      !this.config.value ||
      (this.config.value.nodes || []).length === 0
    ) {
      return
    }

    const { nodeType, edgeType } = this.config
    const tooltip = new G6.Tooltip({
      getContent(event) {
        if (event?.item?.getType() === 'combo') {
          return ''
        }
        if (event?.item?.getType() !== 'node') {
          const sourceLabel = (event?.item as IEdge).getSource().getModel()
            .label
          const targetLabel = (event?.item as IEdge).getTarget().getModel()
            .label
          return `<div class='dagre-tooltip'>
            <span>${sourceLabel} —> ${targetLabel}</span>
          </div>`
        }
        return `<div class='dagre-tooltip'>
          <span>${event?.item?.getModel().label}</span>
        </div>`
      },
    })

    const graphConfig: GraphOptions = {
      container: this.$el as HTMLElement,
      width: this.config.size[0] - this.graphPadding[0],
      height: this.config.size[1] - this.graphPadding[1],
      plugins: [tooltip],
      fitView: true,
      minZoom: 0.7,
      modes: {
        default: [
          'drag-canvas',
          {
            type: 'scroll-canvas',
            direction: 'y',
            scalableRange: -1,
          },
        ],
      },
      layout: {
        type: 'dagre',
        rankdir: 'LR',
        align: 'DL',
        sortByCombo: true,
        controlPoints: true,
        nodesepFunc: () => -15,
        ranksepFunc: () => -15,
      },
      defaultNode: {
        size: [184, 32],
        type: nodeType,
        style: {
          fill: '#7A88F2',
        },
        labelCfg: {
          style: {
            fill: '#ffffff',
          },
        },
      },
      defaultEdge: {
        type: edgeType,
        size: 1,
        color: '#B6BDD2', // 设置透明的 '#B6BDD2',
        style: {
          endArrow: {
            path: G6.Arrow.triangle(3, 5, 0),
            d: 0,
          },
          // radius: 20,
        },
      },
      defaultCombo: {
        type: 'rect',
        style: {
          lineWidth: 1,
        },
        size: [50, 50],
        padding: [55, 20, 15, 20],
        labelCfg: {
          refX: 110,
          position: 'top',
          /* label's style */
          style: {
            fontSize: 18,
            textAlign: 'center',
          },
        },
      },
    }
    const graph = new G6.Graph(graphConfig)
    // @ts-ignore
    graph.data(this.parseDagreData(this.config.value))
    graph.render()
    this.graphInstance = graph
    this.addDagreEdges()
    this.updateDagrePosition()

    graph.on('node:click', (event) => {
      const nodeAttributes: any = event.item?.getModel().attrs
      console.log(nodeAttributes)
      const filterKey = nodeAttributes.find((attribute: any) => {
        return attribute.key === 'filterKey'
      })
      const matchedKey = nodeAttributes.find((attriubte: any) => {
        return attriubte.key === filterKey.value
      })

      this.$emit('dagreNodeClick', matchedKey)
    })
  }

  /**
   * 更新配置，目前只会更新节点类型和edge类型
   */
  private updateDagre(newConfig: IKeyValue, oldConfig: IKeyValue) {
    const { nodeType, edgeType, size } = this.config
    const allNodes = this.graphInstance.getNodes()
    const allEdges = this.graphInstance.getEdges()
    if (
      // update node
      nodeType &&
      allNodes.length > 0 &&
      allNodes[0].getKeyShape().cfg.type !== nodeType
    ) {
      allNodes.forEach((node: any) => {
        node.update({
          type: nodeType,
        })
      })
    }
    if (
      // update edge
      edgeType &&
      allEdges.length > 0 &&
      allEdges[0].getModel().type !== edgeType
    ) {
      allEdges.forEach((edge: any) => {
        edge.update({
          type: edgeType,
        })
      })
    }
    const newWidth = size[0] - this.graphPadding[0]
    const newHeight = size[1] - this.graphPadding[1]
    if (
      this.graphInstance.getWidth() !== newWidth ||
      this.graphInstance.getHeight() !== newHeight
    ) {
      this.graphInstance.changeSize(newWidth, newHeight)
      this.graphInstance.fitView(0)
    }

    const oldNodeIds = allNodes.map((node: any) => node.getModel().id)
    const newValue = this.parseDagreData(this.config.value)
    const newNodeIds = newValue.nodes.map((node: any) => node.id)

    if (
      newConfig.dataId !== oldConfig.dataId ||
      !isEqual(oldNodeIds, newNodeIds)
    ) {
      this.graphInstance.changeData(newValue)
      this.addDagreEdges()

      this.updateDagrePosition()
    }
  }

  /**
   * 为dagre 图加边
   */
  private addDagreEdges() {
    const { edges } = this.config.value
    const nodeIdSet = new Set(
      this.graphInstance.getNodes().map((node: any) => node.getModel().id)
    )
    this.graphInstance.getEdges().forEach((edge: any) => {
      this.graphInstance.hideItem(edge)
    })

    const newEdges: any[] = (edges as any[])
      .filter((edge) => {
        return nodeIdSet.has(edge.source) && nodeIdSet.has(edge.target)
      })
      .map((edge) => ({
        id: edge.id,
        type: 'line',
        source: edge.source,
        target: edge.target,
      }))
    newEdges.forEach((edge) => {
      this.graphInstance.addItem('edge', edge)
    })
  }

  private updateDagrePosition() {
    setTimeout(() => {
      const comboWidth = this.graphInstance.getCombos()[0].getBBox().width
      const zoom = this.graphInstance.getZoom()
      const graphWidth = this.graphInstance.getWidth()
      const positionX = (graphWidth - (comboWidth * 3 + 50) * zoom) / 2
      this.graphInstance.moveTo(positionX < 0 ? 0 : positionX, 10)
    }, 0)
  }

  /**
   * 转换数据为lineGraph需要的格式
   */
  private parseLineGraphData() {
    const nodes: any = []
    const edges: any = []
    this.config.value.nodes.forEach((node: any) => {
      nodes.push({
        ...node,
        type: 'node-with-line',
      })
    })
    const { edgeWeightRange } = this.config
    const isFilterEdge =
      Array.isArray(edgeWeightRange) && edgeWeightRange.length === 2 // 筛选边权重
    const weightScale = scaleLinear() // 权重映射阈值
      .domain([0.4, 0.85, 0.9, 0.95, 1])
      .range([1, 2, 4, 6, 9])

    this.config.value.edges.forEach((edge: any) => {
      if (
        isFilterEdge &&
        (edge.weight > edgeWeightRange[1] || edge.weight < edgeWeightRange[0])
      ) {
        return
      }
      edges.push({
        ...edge,
        style: {
          lineWidth: weightScale(edge.weight), // Math.ceil(8 * edge.weight),
          endArrow: this.config.edgeDirected,
        },
        type: this.config.edgeType,
      })
    })

    return {
      nodes,
      edges,
    }
  }

  /**
   * 渲染lineGraph图
   */
  private renderLineGraph() {
    if (
      !this.$el ||
      !this.config.value ||
      (this.config.value.nodes || []).length === 0
    ) {
      return
    }

    const graph = new G6.Graph({
      container: this.$el as HTMLElement,
      width: this.config.size[0] - this.graphPadding[0],
      height: this.config.size[1] - this.graphPadding[1],
      fitView: true,
      animate: true,
      modes: {
        default: ['drag-canvas', 'drag-node'],
      },
      defaultNode: {
        type: 'node-with-line',
      },
      defaultEdge: {
        color: '#6391f2',
      },
    })

    this.graphInstance = graph

    graph.data(this.parseLineGraphData())
    graph.render()

    graph.on('node:click', (event) => {
      const nodeAttributes: any = event.item?.getModel().attrs
      const filterKey = nodeAttributes.find((attribute: any) => {
        return attribute.key === 'filterKey'
      })
      const matchedKey = nodeAttributes.find((attriubte: any) => {
        return attriubte.key === filterKey.value
      })

      this.$emit('lineGraphNodeClick', matchedKey)
    })

    graph.on('node:dragend', () => {
      const allNodes = this.graphInstance.getNodes()
      const allNodesPosition = allNodes.map((node: any) => {
        const { id, x, y } = node.getModel()
        return {
          id,
          x,
          y,
        }
      })
      console.log(allNodesPosition)

      this.$emit('lineGraphNodeDragend', allNodesPosition)
    })
  }

  /**
   * 更新lineGraph，由于line chart在第一次渲染后，resize时会出现错位问题。先简单处理为销毁后重新渲染
   */
  private updateLineGraph() {
    this.graphInstance.destroy()
    this.renderLineGraph()
  }
}
</script>

<style lang="less">
.graph-chart-view {
  .dagre-tooltip {
    padding: 8px;
    width: auto;
  }

  .g6-component-tooltip {
    padding: 0;
  }
}
</style>
